package com.mlming.springboot.service.impl;

import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.google.gson.Gson;
import com.mlming.springboot.consts.MallConst;
import com.mlming.springboot.dao.ProductMapper;
import com.mlming.springboot.enums.ProductStatusEnum;
import com.mlming.springboot.enums.ResponseEnum;
import com.mlming.springboot.pojo.Product;
import com.mlming.springboot.service.CategoryService;
import com.mlming.springboot.service.ProductService;
import com.mlming.springboot.vo.ProductDetailVo;
import com.mlming.springboot.vo.ProductVo;
import com.mlming.springboot.vo.ResponseVo;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;

@Service
@Slf4j
public class ProductServiceImpl implements ProductService {
    @Autowired
    private CategoryService categoryService;

    @Autowired
    private ProductMapper productMapper;

    @Autowired
    private RedisTemplate redisTemplate; // redis依赖提供的内置dao层对象

    @Autowired
    private RedissonClient redissonClient;// Redisson对象: 用于分布式锁的使用

    // private static int MYSQL_count = 0;// 统计并发下, 访问数据库的次数
    // private static int REDIS_count = 0;// 统计并发下, 访问Redis的次数

    @Override
    public ResponseVo<PageInfo> getProducts(Integer categoryId, Integer pageNum, Integer pageSize) {
        // 因为一般而言, 前几页的商品列表是经常要访问的数据, 每次都去数据库查, 十分浪费时间, 所以用redis存下来
        // 对于获取商品列表的请求, 首先从redis里面访问看看有没有这该页对应的responseVO
        // redis数据结构: hash:
        // productList {
        //      页号: ResponseVo<PageInfo>
        //      页号: ResponseVo<PageInfo>
        //      ...
        // }
        // 由于这样无法给单个页面对应的商品列表设置过期时间, 这就无法实现 缓存穿透使用的"缓存无效Key"的策略
        // 所以, 之后采取这样的数据结构: hash:
        // productList+页号(key) : responseVo对象(value)
        ValueOperations<String,ResponseVo<PageInfo>> hashOperations = redisTemplate.opsForValue();
        // 先去redis里面查该页对应的商品列表
        ResponseVo<PageInfo> responseVo = hashOperations.get(MallConst.PRODUCT_LIST_KEY + pageNum);
        // 如果redis没有 才去访问数据库
        if(responseVo == null) {
            // 获取当前页对应的商品列表的分布式锁, 并尝试加锁tryLock
            RLock rLock = redissonClient.getLock(MallConst.PRODUCT_LIST_LOCK_NAME + pageNum);
            boolean isLock = false;
            try {
                isLock = rLock.tryLock(10,3, TimeUnit.SECONDS);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 如果加锁成功, 则执行下面的代码:
            if(isLock) {
                // 双重校验锁机制: 在加锁后, 看看当前这个查询结果是否还是null,
                // 如果不是, 则访问redis, 如果是, 则访问数据库然后存入redis
                // 这样一来, 结合2)的串行化, 当第一个线程进来时, 此时再次访问redis, 肯定还是null, 那就访问数据库获取到数据存入redis,
                // 因为是缓存击穿不是缓存穿透, 所以此时数据库查到的数据不会是null, 则存入redis, 然后释放锁,
                // 串行化第二个线程进来, 再次访问redis时,此时就会因为串行化的第一个线程已经把数据加入到redis了, 所以会直接返回redis查出来的数据
                // 所以后续的线程都会在再次访问redis后,不会再访问数据库, 从而解决了一下子都因为redis没有而去访问数据库的缓存击穿问题
                ResponseVo<PageInfo> checkResponseVO = hashOperations.get(MallConst.PRODUCT_LIST_KEY + pageNum);
                if(checkResponseVO == null) {
                    // log.warn("------------------访问了数据库哦------------------------------" + (++MYSQL_count));
                    // 先获取到参数categoryId以及其下面的所有子目录的categoryid的集合
                    Set<Integer> categoryIdSet = new HashSet<>();
                    if(categoryId != null) {
                        categoryService.findAllCategoryIdsByCategoryId(categoryId,categoryIdSet);
                        categoryIdSet.add(categoryId);
                    }
                    // 通过pageHelper实现分页功能: 会导致紧接着的查询自动加以分页
                    PageHelper.startPage(pageNum,pageSize);
                    // 根据set里面的 categoryId 查询实际Product列表
                    List<Product> products = productMapper.selectByCategoryIdSet(categoryIdSet);
                    // 返回的是ProductVo类,所以要进行转化:
                    List<ProductVo> productVoList = new ArrayList<>();
                    for (Product product : products) {
                        ProductVo productVo = new ProductVo();
                        BeanUtils.copyProperties(product,productVo);
                        productVoList.add(productVo);
                    }
                    // 通过pageHelper加入接口文档中要求的那堆分页参数
                    PageInfo pageInfo = new PageInfo(products);
                    // 因为此处要显示的list与查出来的list不是同一个,所以要setList
                    pageInfo.setList(productVoList);
                    // 更新responseVo作为返回
                    responseVo = ResponseVo.success(pageInfo);

                    // 如果是当前查询到的列表不是空列表, 则把当前页对应的responseVo存入redis,方便下一次访问
                    if(products.size() > 0){
                        hashOperations.set(MallConst.PRODUCT_LIST_KEY+pageNum,responseVo);
                    }
                    // 如果查到的是空列表, 说明这个是 无效Key, 是会引发缓存穿透的无效Key, 所以根据我们采用的 "缓存Key"的策略
                    // 所以, 我们也要缓存该无效页号对应的responseVo, 但是要设置一个比较短的过期时间: 这里设置为3分钟
                    else {
                        hashOperations.set(MallConst.PRODUCT_LIST_KEY+pageNum,responseVo,MallConst.WASTED_KEY_EXPIRE,TimeUnit.MINUTES);
                    }
                } else {
                    // 如果再次查到的responseVo不是null, 则说明这是第二个串行的线程了, 那这重新查询的结果就是要返回的结果, 所以赋值
                    responseVo = checkResponseVO;
                }
                // 无论如何, 最后一定要释放这个锁:
                rLock.unlock();
            }
        }
        // log.warn("------------------访问了Redis哦------------------------------" + (++REDIS_count));
        return responseVo;
    }

    @Override
    public ResponseVo<ProductDetailVo> getProductDetailById(Integer productId) {
        Product product = productMapper.selectByPrimaryKey(productId);
        // 如果商品编号不存在 或者 当前商品状态是已下架或者删除,则返回报错信息
        if(product == null || product.getStatus().equals(ProductStatusEnum.OFF_SALE.getCode()) ||
                product.getStatus().equals(ProductStatusEnum.DELETE.getCode())) {
            return ResponseVo.error(ResponseEnum.PRODUCT_OFF_SALE_OR_DELETE);
        }
        // 否则,返回商品详情
        ProductDetailVo productDetailVo = new ProductDetailVo();
        BeanUtils.copyProperties(product,productDetailVo);
        // 对商品的一些敏感信息进行一些虚假处理
        productDetailVo.setStock(productDetailVo.getStock() > 100 ? 100 : productDetailVo.getStock());
        return ResponseVo.success(productDetailVo);
    }
}
